import { NextRequest, NextResponse } from "next/server"; import { and, eq, sql } from "drizzle-orm"; import { db } from "@/db/client"; import { listingMedia } from "@/db/schema"; import { assertSameOrigin, requireUser } from "@/lib/auth/session"; import { getListingFull, assertOwner } from "@/lib/listings/service"; import { processUpload, MediaError, MAX_BYTES } from "@/lib/listings/media"; import { apiError, handle } from "@/lib/api"; import { audit } from "@/lib/auth/audit"; export const dynamic = "force-dynamic"; export async function PUT(req: NextRequest, { params }: { params: Promise<{ token: string }> }) { try { await assertSameOrigin(); const u = await requireUser(); const { token } = await params; const d = db(); const [m] = await d.select().from(listingMedia).where(and(eq(listingMedia.uploadToken, token), eq(listingMedia.status, "UPLOADING"), sql`${listingMedia.uploadedAt} > now() - interval '15 minutes'`)).limit(1); if (!m) return apiError(404, "not_found", "Jeton d'envoi invalide ou expiré"); const l = await getListingFull(m.listingId); if (!l) return apiError(404, "not_found", "Annonce introuvable"); assertOwner(l, u.id, u.roles); const len = Number(req.headers.get("content-length") ?? 0); if (len > MAX_BYTES) return apiError(413, "too_large", "Image trop volumineuse (25 Mo max)"); const buf = Buffer.from(await req.arrayBuffer()); if (buf.length > MAX_BYTES) return apiError(413, "too_large", "Image trop volumineuse (25 Mo max)"); try { const r = await processUpload(buf, req.headers.get("content-type")); await d.update(listingMedia).set({ storageKey: r.key, mimeType: "image/webp", width: r.width, height: r.height, bytes: r.bytes, scanStatus: "CLEAN", status: "READY", uploadToken: null }).where(eq(listingMedia.id, m.id)); await audit("media.upload", { actorId: u.id, objectType: "listing_media", objectId: m.id, meta: { bytes: buf.length, mime: r.mime } }); return NextResponse.json({ ok: true, mediaId: m.id, url: `/api/media/${r.key}/800.webp`, width: r.width, height: r.height }); } catch (e) { await d.update(listingMedia).set({ status: "DELETED", scanStatus: "REJECTED", uploadToken: null }).where(eq(listingMedia.id, m.id)); if (e instanceof MediaError) { await audit("media.rejected", { actorId: u.id, objectId: m.id, meta: { code: e.code } }); return apiError(422, e.code, e.message); } throw e; } } catch (e) { return handle(e); } }